package ro.utcn.oop.mandelbrot.gui;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.swing.JPanel;

import ro.utcn.oop.mandelbrot.model.MandelbrotPoint;
import ro.utcn.oop.mandelbrot.rmi.client.MandelbrotServicePool;
import ro.utcn.oop.mandelbrot.rmi.server.IMandelbrotService;
import ro.utcn.oop.mandelbrot.rmi.server.MandelbrotService;

public class MandelbrotPanel extends JPanel implements MouseListener,  
		MouseMotionListener {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
//de la screen
	public static final int WIDTH = 1050;
	public static final int HEIGHT = 600;
//o patrime din valorile W, H...o zona de 16 ori mai mica decat zona initiala se seteaza cat zona initiala(la zoom)
	public static final int ZOOM_WIDTH = WIDTH / 4;
	public static final int ZOOM_HEIGHT = HEIGHT / 4;
//pct de start din care incepe sa se deseneze in panel: marginea alba care ramane langa albastru
	public static final int DEFX = 6;
	public static final int DEFY = 6;
//coord pe set
	public static final double DEFAULT_MIN_X = -2.5;
	public static final double DEFAULT_MIN_Y = -1;
	public static final double DEFAULT_MAX_X = 1;
	public static final double DEFAULT_MAX_Y = 1;

	/** Reference to the panel graphich2D object. */
	private Graphics2D g2d;//clasa care se ocupa de desenare
//valorile afisate pe gui, coordonatele in xoy, limitele afisate pe screen, pe masura ce facem zoom, se calc pt zona aia
	private double minX;
	private double minY;
	private double maxX;
	private double maxY;

	private boolean zooming;//true cand se face zoom

	private int maxIterationNumber;


	private int[][] fractal;//matrice de pixeli care tine nr de iteratii pt fiecare pixel...se calculeaza pt fiecare pixel
	private int zoomCornerX;//coltul unde e pozitionat ala verde
	private int zoomCornerY;

	/**
	 * The constructor of the MandelbrotPanel class. Initializes the mandelbrot
	 * panel.
	 * 
	 * @param iterationNumber
	 * 
	 */
	public MandelbrotPanel(int iterationNumber) {
		this.maxIterationNumber = iterationNumber;//primeste iteration number, il seteaza
		this.setPreferredSize(new Dimension(WIDTH + 5, HEIGHT + 5));//cat de mare sa fie screenul
		this.setOpaque(false);
		this.addMouseListener(this);//adauga pe clasa curenta
		this.addMouseMotionListener(this);
		resetLimits();//apelez reset limits, max x la default. max x
		fractal = new int[WIDTH][HEIGHT];//instantiaza matrice
		calculateFractal();//calculeaza imaginea care se afiseaza initial
	}

	private void calculateFractal() {
		final List<Thread> workerThreads = new ArrayList<Thread>();//lista de treaduri

		// Initialize matrix
		for (int i = 0; i < WIDTH; i++) {
			for (int j = 0; j < HEIGHT; j++) {
				fractal[i][j] = 0;//se face albastru tot
			}
		}

		startCalculationThread(workerThreads, 0, 0, WIDTH / 2, HEIGHT / 2);

		// startCalculationThread(workerThreads, 0, 0, WIDTH / 4, HEIGHT / 4);
		// startCalculationThread(workerThreads, WIDTH / 4, 0, WIDTH / 2,
		// HEIGHT / 4);
		// startCalculationThread(workerThreads, 0, HEIGHT / 4, WIDTH / 4,
		// HEIGHT / 2);
		// startCalculationThread(workerThreads, WIDTH / 4, HEIGHT / 4, WIDTH /
		// 2,
		// HEIGHT / 2);

		startCalculationThread(workerThreads, WIDTH / 2, 0, WIDTH, HEIGHT / 2);

		// startCalculationThread(workerThreads, WIDTH / 2, 0, 3 * WIDTH / 4,
		// HEIGHT / 4);
		// startCalculationThread(workerThreads, 3 * WIDTH / 4, 0, WIDTH,
		// HEIGHT / 4);
		// startCalculationThread(workerThreads, WIDTH / 2, HEIGHT / 4,
		// 3 * WIDTH / 4, HEIGHT / 2);
		// startCalculationThread(workerThreads, 3 * WIDTH / 4, HEIGHT / 4,
		// WIDTH,
		// HEIGHT / 2);

		startCalculationThread(workerThreads, 0, HEIGHT / 2, WIDTH / 2, HEIGHT);

		// startCalculationThread(workerThreads, 0, HEIGHT / 2, WIDTH / 4,
		// 3 * HEIGHT / 4);
		// startCalculationThread(workerThreads, WIDTH / 4, HEIGHT / 2, WIDTH /
		// 2,
		// 3 * HEIGHT / 4);
		// startCalculationThread(workerThreads, 0, 3 * HEIGHT / 4, WIDTH / 4,
		// HEIGHT);
		// startCalculationThread(workerThreads, WIDTH / 4, 3 * HEIGHT / 4,
		// WIDTH / 2, HEIGHT);
		//
		startCalculationThread(workerThreads, WIDTH / 2, HEIGHT / 2, WIDTH,
				HEIGHT);
		//
		// startCalculationThread(workerThreads, WIDTH / 2, HEIGHT / 2,
		// 3 * WIDTH / 4, 3 * HEIGHT / 4);
		// startCalculationThread(workerThreads, 3 * WIDTH / 4, HEIGHT / 2,
		// WIDTH,
		// 3 * HEIGHT / 4);
		// startCalculationThread(workerThreads, WIDTH / 2, 3 * HEIGHT / 4,
		// 3 * WIDTH / 4, HEIGHT);
		// startCalculationThread(workerThreads, 3 * WIDTH / 4, 3 * HEIGHT / 4,
		// WIDTH, HEIGHT);

		// Wait for Threads
		new Thread() { //creez un tread care sa faca afisarea din cand in cand a progresului celorlalte 4 treaduri

			public void run() {//metoda run e suprascrisa
				long lastDisplayTime = 0;
				while (workerThreads.size() > 0) { //cat timp este un tread care ruleaza
					long curentTime = System.currentTimeMillis();
					if (lastDisplayTime == 0 //daca nu a fost afisat niicodata nimic
							|| lastDisplayTime + 500 < curentTime) { //au trecut 500 de cand a fost afisat ceva
						repaint();//apelez repaint
						lastDisplayTime = System.currentTimeMillis();//timpul cand a fost afisat ultima data ceva pe screen
					}
					try {
						Thread.sleep(100);//sleep
					} catch (Exception e) {
						e.printStackTrace();
					}
					checkThreads(workerThreads);//se scoate tredul din lista
				}
				repaint();//repaint dupa ce s-a terminat de executat cele 4 treaduri
			};
		}.start();//treadul asta porneste, un refresh al screenului sa se vada progresul
	}

	private void startCalculationThread(List<Thread> workerThreads,//porneste unul din treduri
			final int startX, final int startY, final int endX, final int endY) {
		Thread worker = new Thread() {// suprascrie metoda run
			public void run() {
				
				// ---------------With RMI--------------------------
				// IMandelbrotService mandelbrotService = MandelbrotServicePool
				// .getInstance().getFreeMandelbrotServiceProxy();
				// ----------------Without RMI--------------------------
				IMandelbrotService mandelbrotService = new MandelbrotService();
				// -------------------------------------------------------
				if (mandelbrotService != null) {//daca a gasit un mandelbrot service ia fiecare pixel , converteste coord din pixeli in point mandelbrot, si apeleaza metoda , result il pune in fractal
					for (int i = startX; i < endX; i++) {
						for (int j = startY; j < endY; j++) {
							MandelbrotPoint point = convertCoordonates(i, j);
							int result = mandelbrotService.calculate(point
									.getX(), point.getY(), maxIterationNumber);
							fractal[i][j] = result;
						}
					}
					
					MandelbrotServicePool.getInstance()
							.releaseMandelbrotServiceProxy(mandelbrotService);//se lasa proxy ca alt trea sa il foloseasca dupa ce s-a desenat
				}
			};
		};
		workerThreads.add(worker);//dauga tredul si da start la worker...incepe sa calculeze bucatica
		worker.start();
	}

	private void checkThreads(List<Thread> workerThreads) {//iteratorul itreaza elementele din lista
		Iterator<Thread> iterator = workerThreads.listIterator();
		while (iterator.hasNext()) {//cat are urmator, obtin urmator, daca tredul  a terminat il scot din lista
			Thread thread = iterator.next();
			if (!thread.isAlive()) {
				iterator.remove();
			}
		}
	}

	/**
	 * Creates a alpha composite for the given alpha.
	 * 
	 * @param alpha
	 *            the alpha to use.
	 * @return the created alpha composite.
	 */
	private AlphaComposite makeComposite(double alpha) {//cat de transparent sa fie ce afisez pe gui
		int type = AlphaComposite.SRC_OVER;
		return (AlphaComposite.getInstance(type, (float) alpha));
	}

	/**
	 * Draws the borders.
	 */
	private void drawBorders() {
		g2d.setPaint(Color.white);
		g2d.fill(new Rectangle2D.Double(DEFX, DEFY, WIDTH + 1, HEIGHT + 1));//screenul mare e desenat alb
		g2d.setPaint(Color.black);
		g2d.setComposite(makeComposite(0.3));
		g2d.draw(new Rectangle2D.Double(DEFX, DEFY, WIDTH + 1, HEIGHT + 1));//deseneaza dreptunghiul negru cu transparenta 30%
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void paintComponent(Graphics g) {//se apeleaza de fiecare data cand apelez repaint
		super.paintComponent(g);
		g2d = (Graphics2D) g;//setez g2d pe care se face desenarea
		BasicStroke bsLine = new BasicStroke(1, BasicStroke.CAP_SQUARE,// defineste cum sa arate linia, cat de groasa, colturi rotunjite
				BasicStroke.JOIN_MITER);
		g2d.setStroke(bsLine);//seteaza tipul de linie care sa fie afisat de acuma incolo
		drawBorders();//liniile o sa fie de tipul de sus
		if (maxIterationNumber > 0) {
			drawFractal();//deseneaza desenul
		}
		if (zooming) {
			drawZoomRectangle();
		}
	}

	private void drawZoomRectangle() {
		g2d.setPaint(Color.green);
		g2d.setComposite(makeComposite(0.9));
		g2d.draw(new Rectangle2D.Double(zoomCornerX, zoomCornerY, ZOOM_WIDTH,
				ZOOM_HEIGHT));
	}

	private void drawFractal() {
		g2d.setComposite(makeComposite(1));//sa fie netransparent
		for (int i = DEFX + 1; i < WIDTH + 1 + DEFX; i++) {//parcurg matricea
			for (int j = DEFY + 1; j < HEIGHT + 1 + DEFY; j++) {
				int result = fractal[i - 7][j - 7];//iteratia a cata a iesit in algoritm
				if (result == maxIterationNumber) {
					g2d.setPaint(Color.BLACK);
				} else if (result < maxIterationNumber * 0.05) {
					g2d.setPaint(Color.BLUE);
				} else {
					int color = result * 255 / maxIterationNumber;
					g2d.setPaint(new Color(color, color, color));
					// g2d.setPaint(new Color(color, (color + color) % 256,
					// (color + color + color) % 256));
					// g2d.setPaint(new Color(color, (color * color) % 256,
					// (color * color * color) % 256));
				}

				g2d.drawLine(i, j, i, j);//deseanza o linie de un punct
			}
		}

	}

	private MandelbrotPoint convertCoordonates(int x0, int y0) {//din pixeli in -2.5//
		MandelbrotPoint point = new MandelbrotPoint();
		double x = x0 * (maxX - minX) / WIDTH;
		x = x + minX;
		double y = y0 * (maxY - minY) / HEIGHT;
		y = y + minY;
		point.setX(x);
		point.setY(y);
		return point;
	}

	public void zoom() {
		zooming = true;
		zoomCornerX = DEFX + 1;//il seteaza in colt pe chenrul verde si afiseaza chenarul
		zoomCornerY = DEFY + 1;
		repaint();
	}

	public void reset() {
		zooming = false;
		resetLimits();
		calculateFractal();
		repaint();

	}

	private void resetLimits() {
		minX = DEFAULT_MIN_X;//la -2.5, 1
		minY = DEFAULT_MIN_Y;
		maxX = DEFAULT_MAX_X;
		maxY = DEFAULT_MAX_Y;
	}

	public void setIterationNumber(int iterationNumber) {//apelata cand se schimba sliderul
		zooming = false;
		maxIterationNumber = iterationNumber;
		calculateFractal();
		repaint();
	}

	@Override
	public void mouseDragged(MouseEvent e) {
		// TODO Auto-generated method stub
	}

	@Override
	public void mouseMoved(MouseEvent e) {//cand suntem in zoom 
		if (zooming) {
			int x = e.getX();//la ce coordonate s-a intamplat eventul
			int y = e.getY();

			x = x - ZOOM_WIDTH / 2;//aflam coltul din stg sus
			y = y - ZOOM_HEIGHT / 2;
			if (x < DEFX + 1) {//daca x e undeva mai mic decat marginea din stg a ecranului atunci il pune in marginea din stg
				x = DEFX + 1;
			}
			if (y < DEFY + 1) {
				y = DEFY + 1;
			}
			if (x + ZOOM_WIDTH > WIDTH + DEFX) {
				x = WIDTH + DEFX - ZOOM_WIDTH;
			}
			if (y + ZOOM_HEIGHT > HEIGHT + DEFY) {
				y = HEIGHT + DEFY - ZOOM_HEIGHT;
			}
			zoomCornerX = x;//pt a afisa patratica verde
			zoomCornerY = y;
			repaint();

		}

	}

	@Override
	public void mouseClicked(MouseEvent e) {
		// TODO Auto-generated method stub
	}

	@Override
	public void mouseEntered(MouseEvent e) {
		// TODO Auto-generated method stub
	}

	@Override
	public void mouseExited(MouseEvent e) {
		// TODO Auto-generated method stub
	}

	@Override
	public void mousePressed(MouseEvent e) {
		// TODO Auto-generated method stub
	}

	@Override
	public void mouseReleased(MouseEvent e) {
		if (zooming) {
			int x = e.getX();//ia coordonatele de unde am vrut sa fac zoom
			int y = e.getY();

			// validate x,y
			x = x - ZOOM_WIDTH / 2;//le pune in coltul din stg sus
			y = y - ZOOM_HEIGHT / 2;
			if (x < DEFX + 1) {
				x = DEFX + 1;
			}
			if (y < DEFY + 1) {
				y = DEFY + 1;
			}
			if (x + ZOOM_WIDTH > WIDTH + DEFX) {
				x = WIDTH + DEFX - ZOOM_WIDTH;
			}
			if (y + ZOOM_HEIGHT > HEIGHT + DEFY) {
				y = HEIGHT + DEFY - ZOOM_HEIGHT;
			}
			int newMinX = x;//noile coordonate ale colturilor sus, stg ale screenului de zoom, le converteste in coordonate xoy
			int newMinY = y;
			int newMaxX = newMinX + ZOOM_WIDTH;
			int newMaxY = newMinY + ZOOM_HEIGHT;

			MandelbrotPoint minPoint = convertCoordonates(newMinX, newMinY);
			MandelbrotPoint maxPoint = convertCoordonates(newMaxX, newMaxY);

			minX = minPoint.getX();
			minY = minPoint.getY();
			maxX = maxPoint.getX();
			maxY = maxPoint.getY();
			zooming = false;
			calculateFractal();
			repaint();//afiseaza doar bucatica la care am facut zoom
		}
	}

}
